package com.foodev.maven.gwt;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;

/**
 * @author Martin Algesten
 */
public abstract class AbstractGwtMojo extends AbstractMojo {

    public static final String OS_NAME = System.getProperty("os.name").toLowerCase(Locale.US);


    protected static boolean isWindows() {
        return OS_NAME.startsWith("windows");
    }


    protected static boolean isLinux() {
        return OS_NAME.startsWith("linux");
    }


    protected static boolean isMac() {
        return OS_NAME.startsWith("mac");
    }


    /**
     * @parameter expression="${project.build.directory}/gwt"
     */
    protected File gwtHome;

    /**
     * The output for the gwt compilation. This is by default set to <code>${basedir}/src/main/webapp</code> so that
     * plugins like jetty:run can find the compiled app.
     * 
     * @parameter expression="${basedir}/src/main/webapp"
     * @required
     */
    protected File webappSrc;

    /**
     * The maven project.
     * 
     * @parameter expression="${project}"
     * @required
     */
    protected MavenProject project;

    /**
     * The modules
     * 
     * @required
     * @parameter
     */
    protected List<String> modules;


    /**
     * Constructs a classpath from <code>compileSourceRoots + resources +
     * compileClasspathElements + context classloader classpath</code>.
     * 
     * @return the total classpath as a list of strings. No filtering is done, the called must do that with
     *         filterClasspath().
     * @throws MojoExecutionException
     *             if context classloader or local classloader is not of type <code>URLClassLoader</code>
     * @throws MalformedURLException
     */
    @SuppressWarnings("unchecked")
    protected List<String> buildClasspath() throws MojoExecutionException, DependencyResolutionRequiredException {

        HashSet<String> cp = new HashSet<String>();

        cp.addAll(project.getCompileSourceRoots());

        for (Object o : project.getResources()) {

            Resource r = (Resource) o;

            cp.add(r.getDirectory());

        }

        Map<String, MavenProject> projectRefs = project.getProjectReferences();

        for (MavenProject refProj : projectRefs.values()) {
            cp.add(refProj.getBuild().getOutputDirectory());
        }

        cp.addAll(project.getCompileClasspathElements());
        cp.addAll(project.getRuntimeClasspathElements());
        cp.addAll(project.getTestClasspathElements());

        if (Thread.currentThread().getContextClassLoader() instanceof URLClassLoader) {
            cp.addAll(urlToString(Arrays.asList(((URLClassLoader) Thread.currentThread().getContextClassLoader())
                    .getURLs())));
        } else if (getClass().getClassLoader() instanceof URLClassLoader) {
            cp.addAll(urlToString(Arrays.asList(((URLClassLoader) getClass().getClassLoader()).getURLs())));
        } else {
            throw new MojoExecutionException("Failed to get URLClassLoader for plugin");
        }

        return new LinkedList<String>(cp);

    }


    /**
     * Executes the given command in the given working directory.
     * 
     * @param workingDir
     *            the working directory
     * @param cmd
     *            the command with arguments.
     * @return the exit value from the process.
     * @throws IOException
     */
    protected int executeProcess(File workingDir, String[] cmd) throws IOException {

        ProcessBuilder pb = new ProcessBuilder(cmd);
        pb.directory(workingDir);

        pb.redirectErrorStream(true);

        Process proc = pb.start();

        InputStream is = proc.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br = new BufferedReader(isr);

        String line;

        while ((line = br.readLine()) != null) {
            if (getLog().isDebugEnabled()) {
                getLog().debug(line);
            } else {
                getLog().info(line);
            }
        }

        try {
            proc.waitFor();
        } catch (Exception e) {
        }

        return proc.exitValue();

    }


    private final static String ZIPREGEX = ".*[\\\\/]gwt-dev-[^-]+-(mac|linux|windows)-libs\\.zip$";

    // Note that 'target/classes' needs to be on the classpath for
    // the shell, but not for the compilation.
    private final static String[] FILTEREDREGEX = new String[] { ".*[\\\\/]gwt-servlet-.+\\.jar$",
            ".*[\\\\/]plexus-utils-.+\\.jar$" };


    /**
     * Tests if a given classpath element matches the regular expression we use to detect the native library zip.
     * 
     * @param element
     *            the element to test
     * @return true if the given element is the native library zip.
     */
    protected boolean isNativeLibZip(String element) {
        return element.matches(ZIPREGEX);
    }


    /**
     * Filter the classpath given. Parameter tells if we are to filter out the 'target/classes' element.
     */
    protected List<String> filterClasspath(List<String> classpath, boolean filterBuildOutput) {

        LinkedList<String> result = new LinkedList<String>(classpath);

        for (Iterator<String> iter = result.iterator(); iter.hasNext();) {

            String element = iter.next();

            if (isNativeLibZip(element) || isFiltered(element, filterBuildOutput)) {
                iter.remove();
            }

        }

        return result;

    }


    /**
     * Tests if the given classpath element matches a regular expression for filtered elements such as plexus-utils and
     * maven-gwt-plugin.
     * 
     * @param element
     *            the element to test
     * @return true if the given element is filtered.
     */
    private boolean isFiltered(String element, boolean filterBuildOutput) {
        String localBuildOutput = project.getBuild().getOutputDirectory();
        String testBuildOutput = project.getBuild().getTestOutputDirectory();
        for (int i = 0; i < FILTEREDREGEX.length; i++) {
            if (element.matches(FILTEREDREGEX[i]) || filterBuildOutput
                    && (element.equals(localBuildOutput) || element.equals(testBuildOutput)))
                return true;
        }
        return false;
    }


    /**
     * Translates a list of URL to a list of String.
     * 
     * @param urls
     *            the urls to transate
     * @return a list of strings.
     */
    private List<String> urlToString(List<URL> urls) {

        LinkedList<String> str = new LinkedList<String>();

        for (URL url : urls) {

            if (!url.getProtocol().equals("file")) {
                getLog().error("Classpath contains non-file URL: " + url.toString());
                continue;
            }

            String path;

            if (isWindows()) {

                String tmp = url.getPath();
                tmp = tmp.replaceAll(" ", "%20");

                try {

                    url = new URL("file", "", tmp);
                    path = new File(url.toURI()).getAbsolutePath();

                } catch (Exception e) {
                    getLog().error(e);
                    continue;
                }

            } else {

                path = url.getPath();

            }

            str.add(path);

        }

        return str;

    }


    /**
     * Finds the native zip on the classpath, first looks at the thread context classloader, then the class classloader.
     * 
     * @return the URL for the found zip or null if not found.
     * @throws MojoExecutionException
     */
    protected URL findNativeZip() {

        URL url = null;
        ClassLoader cl = null;

        cl = Thread.currentThread().getContextClassLoader();
        if (!(cl instanceof URLClassLoader)) {
            getLog().info("Context ClassLoader is not a URLClassLoader");
        } else {
            url = findZip((URLClassLoader) cl);
        }

        if (url != null)
            return url;

        cl = getClass().getClassLoader();
        if (!(cl instanceof URLClassLoader)) {
            getLog().info("Local ClassLoader is not a URLClassLoader");
        } else {
            url = findZip((URLClassLoader) cl);
        }

        if (url != null)
            return url;

        return null;

    }


    /**
     * Goes through the URLs in a URLClassLoader and find the entry matching the name in ZIPREGEX.
     * 
     * @param cl
     *            the classloader to inspect.
     * @return the found .zip or null.
     */
    private URL findZip(URLClassLoader cl) {

        URL[] urls = cl.getURLs();

        for (int i = 0; i < urls.length; i++) {

            if (isNativeLibZip(urls[i].toString())) {

                return urls[i];

            }

        }

        return null;

    }


}
